﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerSide
{
    public class AsyncSocket
    {
        #region Variables

        /// <summary>
        /// References a method to be called when a corresponding asynchronous operation completes
        /// </summary>
        private AsyncCallback m_pfnWorkerCallBack;

        /// <summary>
        /// Socket to listen connection from clients
        /// </summary>
        private Socket m_socListener;

        /// <summary>
        /// Array socket worker to send and receive data
        /// </summary>
        private Socket[] m_workerSocket = new Socket[2];

        /// <summary>
        /// A number of client connect to server
        /// </summary>
        private int m_clientCount = 0;

        /// <summary>
        /// Event handler to transfer data to other forms
        /// </summary>
        public EventHandler Handler { get; set; }

        /// <summary>
        /// Result for each game of players
        /// </summary>
        public Dictionary<string, string> DictPlayerResult { get; set; }

        /// <summary>
        /// Dictionary to indentify clients
        /// </summary>
        public Dictionary<string, Socket> DictClientSocket { get; set; }

        #endregion

        #region Methods

        /// <summary>
        /// Convert IP Address string to Socket IP Address
        /// </summary>
        /// <param name="interProAddress"></param>
        /// <returns></returns>
        private IPAddress RetrieveIPAddress(string interProAddress)
        {
            IPAddress listenIP = null;
            if (Common.IsLocalhost(interProAddress))
            {
                Common.Logger.Debug("Listen any on localhost");
                // Listen any
                listenIP = IPAddress.Any;
            }
            else
            {
                if (Common.IsIPAddress(interProAddress))
                {
                    if (Common.IsLoopback(interProAddress))
                    {
                        Common.Logger.Debug("Loopback adapter are configured. Listen any on localhost");
                        // Listen any
                        listenIP = IPAddress.Any;
                    }
                    else
                    {
                        Common.Logger.Debug("Listen on specified IP " + interProAddress);
                        // Listen on specified IP
                        listenIP = IPAddress.Parse(interProAddress);
                    }
                }
                else
                {
                    if (Common.IsCorrectHostName(interProAddress))
                    {
                        Common.Logger.Debug("Listen any on " + interProAddress);
                        // Listen any
                        listenIP = IPAddress.Any;
                    }
                    else
                    {
                        Common.Logger.Error("Invalid host name " + interProAddress);
                    }
                }
            }
            return listenIP;
        }

        public bool Listen(string serverAddress, string port)
        {
            try
            {
                // Create the listening socket...
                m_socListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint ipLocal = new IPEndPoint(RetrieveIPAddress(serverAddress), int.Parse(port));

                // Bind to server IP Address...
                m_socListener.Bind(ipLocal);

                // Start listening, the 4 is a parameter indicating backlog indicating the maximum length of the queue of pending connections. 
                m_socListener.Listen(4);

                // Create the call back for any client connections...
                m_socListener.BeginAccept(new AsyncCallback(OnClientConnect), null);
                this.Handler(this, new SocketEvent("NOTIFY:Server is running..."));
                this.DictClientSocket = new Dictionary<string, Socket>();

                return true;
            }
            catch (SocketException se)
            {
                Common.Logger.Error(se.Message);
                this.Handler(this, new SocketEvent("ERROR:" + se.Message));
                return false;
            }
        }

        /// <summary>
        /// Close all sockets to stop listening
        /// </summary>
        public void CloseSockets()
        {
            if (this.m_socListener != null)
            {
                this.m_socListener.Close();
            }
            for (int i = 0; i < this.m_clientCount; i++)
            {
                if (this.m_workerSocket[i] != null)
                {
                    this.m_workerSocket[i].Close();
                    this.m_workerSocket[i] = null;
                }
            }
            this.m_clientCount = 0;
            if (DictClientSocket != null)
            {
                this.DictClientSocket.Clear();
            }
        }

        /// <summary>
        /// When a client made a request connection, this callback routine is called
        /// </summary>
        /// <param name="asyn">
        /// The asyn object
        /// </param>
        public void OnClientConnect(IAsyncResult asyn)
        {
            try
            {
                if (this.m_clientCount == 2)
                {
                    var soc = this.m_socListener.EndAccept(asyn);
                    byte[] bytes = Encoding.ASCII.GetBytes("MAXIMUMCONNECTION");
                    soc.Send(bytes);

                    // Continue listening other clients
                    this.m_socListener.BeginAccept(new AsyncCallback(this.OnClientConnect), null);
                    
                    return;
                }

                // Accept the connection by calling EndAccept
                this.m_workerSocket[this.m_clientCount] = this.m_socListener.EndAccept(asyn);
                WaitForData(this.m_workerSocket[this.m_clientCount]);

                // Increare a number of client connect to server
                this.m_clientCount++;

                // Continue listening other clients
                this.m_socListener.BeginAccept(new AsyncCallback(this.OnClientConnect), null);
            }
            catch (ObjectDisposedException)
            {
                Common.Logger.Error("Socket has been closed");
                this.Handler(this, new SocketEvent("ERROR:Socket has been closed"));
            }
            catch (SocketException se)
            {
                Common.Logger.Error(se.Message);
                this.Handler(this, new SocketEvent("ERROR:" + se.Message));
            }

        }

        /// <summary>
        /// Turn calls BeginReceive for the m_socWorker
        /// </summary>
        /// <param name="soc"></param>
        public void WaitForData(Socket soc)
        {
            try
            {
                if (m_pfnWorkerCallBack == null)
                {
                    m_pfnWorkerCallBack = new AsyncCallback(OnDataReceived);
                }
                SocketPacket theSocPkt = new SocketPacket();
                theSocPkt.thisSocket = soc;
                // now start to listen for any data...
                soc.BeginReceive(theSocPkt.dataBuffer, 0, theSocPkt.dataBuffer.Length, SocketFlags.None, m_pfnWorkerCallBack, theSocPkt);
            }
            catch (SocketException se)
            {
                Common.Logger.Error(se.Message);
                this.Handler(this, new SocketEvent("ERROR:" + se.Message));
            }

        }

        /// <summary>
        /// Receive data from clients
        /// </summary>
        /// <param name="asyn"></param>
        public void OnDataReceived(IAsyncResult asyn)
        {
            try
            {
                SocketPacket theSockId = (SocketPacket)asyn.AsyncState;
                //end receive...
                int iRx = 0;
                iRx = theSockId.thisSocket.EndReceive(asyn);
                char[] chars = new char[iRx + 1];
                System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder();
                int charLen = d.GetChars(theSockId.dataBuffer, 0, iRx, chars, 0);
                System.String szData = new System.String(chars);
                szData = szData.Remove(szData.Length - 1);

                var status = string.Empty;
                string[] compStr = szData.Split(new char[] { ':' });
                switch (compStr[0])
                {
                    case "CONNECT":
                        this.DictClientSocket.Add(compStr[1], theSockId.thisSocket);
                        status = string.Format("Player {0} connected", compStr[1]);

                        // Wait for opponent to start game
                        if (this.m_clientCount < 2)
                        {
                            var msg = "WAITOPPONENT";
                            byte[] bytes = Encoding.ASCII.GetBytes(msg);
                            theSockId.thisSocket.Send(bytes);
                            this.Handler(this, new SocketEvent("NOTIFY:CONNECT#" + compStr[1] + "#" + status));
                        }
                        else
                        {
                            this.DictPlayerResult = new Dictionary<string, string>();
                            this.Handler(this, new SocketEvent("NOTIFY:CONNECT#" + compStr[1] + "#" + status));
                            this.StartGame();
                        }
                        // Continue wait for data
                        WaitForData(theSockId.thisSocket);
                        break;

                    case "DISCONNECT":
                        this.DictClientSocket.Remove(compStr[1]);
                        status = string.Format("Player {0} disconnected", compStr[1]);
                        this.Handler(this, new SocketEvent("NOTIFY:DISCONNECT#" + compStr[1] + "#" + status));
                        this.m_clientCount--;
                        break;

                    case "RESULT":
                        var comp2 = compStr[1].Split(new char[] { '#' });
                        this.Handler(this, new SocketEvent("NOTIFY:" + "Player " + comp2[0] + " give the " + comp2[1]));
                        if (this.DictPlayerResult.Count < 2)
                        {
                            string n;
                            if (!this.DictPlayerResult.TryGetValue(comp2[0], out n))
                            {
                                this.DictPlayerResult.Add(comp2[0], comp2[1]);
                            }
                        }
                        // Update and send result to clients
                        if (this.DictPlayerResult.Count == 2)
                        {
                            var str = "RESULT:";
                            for (int i = 0; i < this.DictPlayerResult.Count; i++)
                            {
                                if (i < this.DictPlayerResult.Count - 1)
                                {
                                    str += (this.DictPlayerResult.ElementAt(i).Key + "#" + this.DictPlayerResult.ElementAt(i).Value + "@");
                                }
                                else
                                {
                                    str += (this.DictPlayerResult.ElementAt(i).Key + "#" + this.DictPlayerResult.ElementAt(i).Value);
                                }
                            }
                            this.Handler(this, new SocketEvent(str));
                        }
                        // Continue wait for data
                        WaitForData(theSockId.thisSocket);
                        break;
                }
            }
            catch (ObjectDisposedException)
            {
                Common.Logger.Error("Socket has been closed");
                this.Handler(this, new SocketEvent("ERROR:Socket has been closed"));
            }
            catch (SocketException se)
            {
                Common.Logger.Error(se.Message);
                this.Handler(this, new SocketEvent("ERROR:" + se.Message));
            }
            catch (Exception se)
            {
                this.Handler(this, new SocketEvent("ERROR:" + se.Message));
            }
        }

        /// <summary>
        /// Send result of opponent to client
        /// </summary>
        /// <param name="player"></param>
        /// <param name="result"></param>
        /// <param name="score"></param>
        public void SendResult(string playerName, int result, int score, string opponentResult)
        {
            try
            {
                var soc = this.DictClientSocket[playerName];
                byte[] bytes = Encoding.ASCII.GetBytes("RESULTSET:" + result.ToString() + "#" + score.ToString() + "#" + opponentResult);
                soc.Send(bytes);
            }
            catch (SocketException se)
            {
                this.Handler(this, new SocketEvent("ERROR:" + se.Message));
            }
        }

        /// <summary>   
        /// Notify to all clients to start game
        /// </summary>
        private void StartGame()
        {
            try
            {
                var msg = "STARTGAME";
                byte[] bytes = Encoding.ASCII.GetBytes(msg);
                for (int i = 0; i < this.m_clientCount; i++)
                {
                    if (this.m_workerSocket[i] != null)
                    {
                        m_workerSocket[i].Send(bytes);
                    }
                }
            }
            catch (SocketException se)
            {
                this.Handler(this, new SocketEvent("ERROR:" + se.Message));
            }
        }

        #endregion
    }
}
